fix(watch): degrade to lexical/graph-only mode on Intel Mac (no vector stack)#446
Merged
Conversation
…le (Intel Mac) `jrag watch` was the only code path that hard-required the vector stack: run_foreground eagerly called warm.model() (pulls sentence_transformers) and watcher.reindex always ran cocoindex. On Intel Mac (darwin + x86_64) PEP 508 markers exclude sentence_transformers/lancedb/cocoindex, so the daemon printed "failed to load embedding model" and exited 2 before serving; and a debounced reindex would bail on a 127 cocoindex stub and never fire indexing_done. Reuse pipeline.vector_stack_installed() (already used everywhere else) to gate: - daemon: skip the model warm-up when vectors are absent; serve warm lexical/graph-only search (the read path, mcp_v2.search_v2, already degrades to lexical on its own). State/panel/--status carry mode=lexical. - watcher: skip the cocoindex reindex step so the graph reindex completes and fires indexing_done instead of erroring forever. Consistent with the cold CLI, which already runs lexical on Intel Mac (#443). Tests: graph-only daemon startup (subprocess exercises real run_foreground; WarmResources.model() raises if called, proving the gating) + graph-only reindex (skips cocoindex, runs graph, fires indexing_done). Existing watcher tests pinned to _vector_enabled=True so they're host-independent. Full suite green (1538 passed, 38 skipped). Co-Authored-By: Claude <noreply@anthropic.com>
Three-reviewer fan-out (core logic / tests / surface+docs) returned "ready to merge" with no Critical/Important correctness issues. Applied the cheap, high-value findings: - skills/explore-codebase-cli/SKILL.md + agents/explorer-rag-cli.md: the shipped watch tip "(no per-call model load)" misrepresents the benefit on Intel Mac (no model exists there). Broadened to "(no per-call model/graph load; warm lexical + graph on Intel Mac)" — accurate for both install flavors. These are verbatim-shipped artifacts consumed by the exact audience this PR enables. - tests/watch/test_daemon.py: the graph-only startup test captured no subprocess log and wasted the full 15s timeout on a regression that exits instantly. Now passes a log_path and polls proc.poll() for an early, diagnosable failure (_fail_with_log surfaces the log tail). - tests/watch/test_watcher.py: graph-only java reindex upgraded from membership asserts to the exact calls-list sequence (matches the sibling vector-path test's style; catches a spurious extra call). - jrag.py: `watch --status` mode line now prints just "lexical (graph-only)", matching the TTY panel and the CLI doc (was "— no vector ranking"). - watch/daemon.py: one-line comment clarifying _state["mode"] derives from the install-time probe, not a live search-capability check (the read path's actual lexical/vector choice is mcp_v2._ensure_vector_backend; they agree under the PEP 508 markers). - watch/watcher.py: wrapped the pipeline import to fit the 100-col line limit. - docs/JAVA-CODEBASE-RAG-CLI.md: hedged the watch intro's "skips the per-call torch/model load" to "model/graph load" + noted graph-only on Intel Mac. Deferred (pre-existing, out of scope): no positive-direction test that the vector-ENABLED daemon warms model() — the vector path was verified byte-unchanged vs base by review; adding it would require pinning _vector_enabled in the shared stub + a cross-process sentinel. Watch tests: 36 passed, 6 skipped. No new ruff errors (the 4 pre-existing jrag.py F-codes exist on origin/master). Co-Authored-By: Claude <noreply@anthropic.com>
CI (test_install_data_artifacts_in_sync_with_dev_source) failed because the review-fix commit edited the dev-source copies in skills/ and agents/ but not the bundled copies under src/java_codebase_rag/install_data/, which the sync check requires to be byte-equal. Regenerated via `scripts/sync_agent_artifacts.py` for the two edited watch tips (explorer-rag-cli.md + explore-codebase-cli/SKILL.md). Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A consumer on Intel Mac (
darwin + x86_64) reportsjrag watchcrashes: it tries to importsentence_transformers, which PEP 508 markers inpyproject.tomlexclude on that platform (sentence-transformers/lancedb/cocoindexare gated tosys_platform != 'darwin' or platform_machine != 'x86_64'). The daemon printsfailed to load embedding model: No module named 'sentence_transformers'and exits 2 before serving.jrag watchwas the only code path that hard-required the vector stack. Every other path already degrades to lexical/graph-only search (mcp_v2.search_v2→_ensure_vector_backend()→ImportError→run_search=None→run_lexical_search; #443 made BM25/lexical first-class).Root cause (two spots)
watch/daemon.pyeagerly calledself.warm.model()to fail-fast on model-load errors. On Intel Mac that pullssentence_transformers→ModuleNotFoundError→ exit 2. (The daemon's read path doesn't even usewarm.model()—server.dispatchpasseswarm.graph()only;search_v2loads the model lazily and falls back to lexical on its own. The eager call was purely fail-fast / first-query optimization.)watch/watcher.py:reindexalways ranrun_cocoindex_updateand treated its nonzero rc as fatal. On Intel Mac that returns a127stub (cocoindex binary absent — no raise), so every debounced reindex emitted a perpetual "vectors" error and never firedindexing_done.Fix
Reuse the existing
pipeline.vector_stack_installed()probe (cheap: 3×importlib.util.find_spec) to gate both spots:watch/daemon.py— probe_vector_enabledin__init__; skip the model warm-up when vectors are absent (print a one-line lexical notice, continue serving). Addmode("lexical"/"vector") to the state file and a panel row.watch/watcher.py— probe_vector_enabledin__init__; inreindex, skip the cocoindex step (and its fatal check) when vectors are off, so the graph reindex still completes and firesindexing_done.jrag.py—watch --statusprintsmode: lexical (graph-only)when applicable.Outcome: the daemon now starts and stays useful on Intel Mac — serving warm lexical
search(BM25 over the symbol graph) plus every structural command (find/inspect/callers/callees/flow), and reindexing the graph on file change.Tests
tests/watch/test_daemon.py— graph-only startup: a subprocess runs the realrun_foregroundwithdaemon.vector_stack_installed→Falseand aWarmResources.model()that raises if called (proving the gating); asserts the daemon reaches serving (not exit 2) and the state file carriesmode: lexical.tests/watch/test_watcher.py— graph-onlyreindex({"java"})skips cocoindex, runs the graph under its COW snapshot lifecycle, and firesindexing_done; a sql change is a clean no-op._vector_enabled = Trueso the vector-path assertions are host-independent (don't implicitly depend on the host's vector stack).Verification
sentence_transformers/torchimport anywhere insrc/java_codebase_rag/watch/.jrag searchreturned hot-served (cold-loads=0),mode=lexical, rc=0, with real symbol results — impossible on the vector path since no vectors were built, proving the lexical path ran over the socket.Docs
docs/JAVA-CODEBASE-RAG-CLI.md,docs/CONFIGURATION.md,docs/ARCHITECTURE.mdnote the lexical/graph-only mode. Skills/agents watch tips stayed accurate (warm lexical is still warm).Out of scope
WarmResources.model()contract (only the daemon warm-up call is gated).pyproject.tomlmarkers unchanged (Intel Mac stays graph-only by design).Related: #435 (lexical-mode direction), #443 (lexical first-class).
🤖 Generated with Claude Code